/** @file f_fstream.cpp
	@brief Simple filestream */

#include "file/f_fstream.h"

using namespace G3;

//==============================================================================
// Constructors & Destructors
//==============================================================================
G3::FILESTREAM::FILESTREAM () {
	Data = NULL;
	CaretPosition = 0;
	BufferPosition = 0;
	BufferedSize = 0;
	TotalSize = 0;

	FileStart = 0;

	FileSize = 0;

	LastLineDelimiter = 0;

	MaximalUnflushed = STREAM_DEF_UNFLUSHED;

	Flags = STREAM_NO_TOTAL_SIZE;

	BufferReadLocked = false;
	BufferWriteLocked = false;
	CaretLocked = false;

	Log = &G3::MainLog;
}

G3::FILESTREAM::~FILESTREAM () {
	if (!BufferWriteLocked && Data != NULL)
		free (Data);
}

//==============================================================================
// Sets stream caret position
//
//		long Offset				File position to seek for
//
//		STREAM_SEEK_FLAG Flag
//			STREAM_BEGINNING	File position caret is reset to the beginning of
//								current stream buffer
//			STREAM_SET			Sets file position to Offset
//
//			STREAM_END			File position caret is moved to the end of the
//								stream
//
//	Return true on success
//==============================================================================
bool G3::FILESTREAM::Seek (long Offset, STREAM_SEEK_FLAG Flag) {
	// Is another thread reading the buffer?
	if (CaretLocked)
		return false;

	long Offs = Offset;

	switch (Flag) {
		case STREAM_BEGINNING:
			if ((Flags & STREAM_ACCUM_BUF) != 0)
				CaretPosition = 0;
			else
				CaretPosition = BufferPosition;
			return true;
		case STREAM_MOVE:
			if (CaretPosition + Offs > TotalSize) {
				if (Log)
					Log->Report ("WARNING! FILESTREAM::Seek: BufferPosition (%ld) + Offset (%ld) too big (TotalSize: %ld)..\n", BufferPosition, Offset, TotalSize);

				CaretPosition = TotalSize;
				return false;
			}
			if (CaretPosition + Offs < BufferPosition) {
				if ((Flags & STREAM_ACCUM_BUF) != 0) {
					if (CaretPosition + Offs < 0) {
						if (Log)
							Log->Report ("WARNING! FILESTREAM::Seek: CaretPosition (%ld) + Offset (%ld) < 0..\n", CaretPosition, Offs);
						CaretPosition = 0;
						return false;
					}
					CaretPosition += Offs;
					return true;
				}
				if (Log)
					Log->Report ("WARNING! FILESTREAM::Seek: CaretPosition (%ld) + Offset (%ld) < BufferPosition (%ld)..\n", CaretPosition, Offs, BufferPosition);

				CaretPosition = BufferPosition;
				return false;
			}
			CaretPosition += Offs;
			return true;
		case STREAM_SET_FILE:
			Offs += BufferPosition;
		case STREAM_SET:
			if (Offs > TotalSize) {
				if (Log)
					Log->Report ("WARNING! FILESTREAM::Seek: Offset (%ld) too big (TotalSize: %ld)..\n", Offs, TotalSize);

				CaretPosition = TotalSize;
				return false;
			}
			if (Offs < 0) {
				if (Log)
					Log->Report ("WARNING! FILESTREAM::Seek: Offset (%ld) smaller than 0..\n", Offs);
				return false;
			}
			if (((Flags & STREAM_ACCUM_BUF) == 0) && Offs < BufferPosition) {
				if (Log)
					Log->Report ("WARNING! FILESTREAM::Seek: Offset (%ld) < BufferPosition (%ld) and STREAM_ACCUM_BUF is not set..\n", Offset, BufferPosition);

				CaretPosition = BufferPosition;
				return false;
			}
			CaretPosition = Offs;
			return true;
		case STREAM_END:
			if (BufferPosition + BufferedSize >= TotalSize)
				CaretPosition = TotalSize;
			else
				CaretPosition = BufferPosition + BufferedSize;
			return true;
	}
	return false;
}

//==============================================================================
// Tells the stream caret position
//
//		STREAM_SEEK_FLAG Flag
//			STREAM_BEGINNING	Tells the BufferPosition of the stream
//
//			STREAM_GET			Tells the CaretPosition of the stream
//
//			STREAM_END			Tells the TotalSize of the stream
//
//			STREAM_SIZE			Tells the BufferedSize of the stream
//
//			STREAM_FILE_SIZE	Tells the current FileSize
//
//	Returns the position in bytes
//==============================================================================
long G3::FILESTREAM::Tell (STREAM_SEEK_FLAG Flag) {
	switch (Flag) {
		case STREAM_BEGINNING:
			return BufferPosition;
		case STREAM_END:
			return TotalSize;
		case STREAM_SIZE:
			return BufferedSize;
		case STREAM_FILE_SIZE:
			return FileSize;
	}
	return CaretPosition;
}

//==============================================================================
// Returns true if End Of File was encountered
//==============================================================================
bool G3::FILESTREAM::EndOfFile () {
	return (CaretPosition == TotalSize);
}

//==============================================================================
// Flushes the stream (has no effect if STREAM_ACCUM_BUF flag is set)
// Frees the part of the buffer that is already read
//
// NOTE:: Very slow
// TODO:: Optimize
//==============================================================================
bool G3::FILESTREAM::Flush (bool Force) {
	long delta = 0;

	// Make sure this flush is needed and that
	// we don't flush the accumulative buffer
	if (BufferPosition == CaretPosition ||
		((Flags & STREAM_ACCUM_BUF) != 0) ||
		Data == NULL)
		return true;

	delta = CaretPosition - BufferPosition;

	Assert (CaretPosition >= BufferPosition, "FILESTREAM::Flush: CaretPosition < BufferPosition but STREAM_ACCUM_BUF flag is not set");
	Assert (BufferedSize - delta >= 0, "FILESTREAM::Flush: CaretPosition - BufferPosition > BufferedSize");

	// Is another thread buffering, reading or flushing the buffer?
	if (BufferReadLocked || BufferWriteLocked || CaretLocked)
		return false;

	// The whole buffer has been read?
	if (BufferedSize - delta == 0) {
		// Totally block the buffer
		BufferReadLocked = true;
		BufferWriteLocked = true;

		BufferPosition = CaretPosition;
		BufferedSize = 0;

		free (Data);
		Data = NULL;

		// Unblock the buffer
		BufferReadLocked = false;
		BufferWriteLocked = false;
		return true;
	}

	// Is there enough data to flush?
	if (delta > MaximalUnflushed || Force) {
		// Totally block the buffer
		BufferReadLocked = true;
		BufferWriteLocked = true;

		memmove ((byte *) Data, (byte *) Data + delta, BufferedSize - delta);

		BufferPosition = CaretPosition;
		BufferedSize -= delta;

		// Unblock the buffer
		BufferReadLocked = false;
		BufferWriteLocked = false;
		return true;
	}
	return false;
}

//==============================================================================
// Just sets the total size of the stream (in bytes)
//==============================================================================
void G3::FILESTREAM::SetTotalSize (long Size) {
	TotalSize = Size;

	Disable (STREAM_NO_TOTAL_SIZE);
}

//==============================================================================
// Just sets the size of current file (in bytes)
//==============================================================================
void G3::FILESTREAM::SetFileSize (long Size) {
	FileSize = Size;
}

//==============================================================================
// Reads data from the stream
//
//		void *Pointer			Pointer to a memory block where the data is stored
//
//		long Size				Number of bytes read from the stream
//
//		bool FlushBuffer		Is the stream buffer flushed after reading?
//				true			The part of the buffer that is already read, is freed
//				false			The data already read is still stored
//
//	NOTE:: FlushBuffer has no effect when STREAM_ACCUM_BUF flag is set
//
//	Returns the number of bytes read
//==============================================================================
long G3::FILESTREAM::Read (void *Pointer, long Size, bool FlushBuffer) {
	long size = Size;

	Assert (Pointer, "FILESTREAM::Read: Pointer argument is a NULL pointer");
	Assert (Data, "FILESTREAM::Read: Data member is a NULL pointer");

	// Is someone flushing / buffering already?
	if (BufferReadLocked)
		return 0;

	// Make sure nobody is moving our caret during the read
	CaretLocked = true;
	// Make sure the buffer data pointer stays constant during our read
	BufferWriteLocked = true;

	// Somebody is trying to read past the end of the file?
	if (Size > BufferedSize - CaretPosition + BufferPosition || Size <= 0)
		size = BufferedSize - CaretPosition + BufferPosition;

	// "Read" the stream
	memcpy (Pointer, (byte *) Data + CaretPosition - BufferPosition, size);

	CaretPosition += size;

	// Free the caret to other threads
	CaretLocked = false;
	// Free the buffer to other threads
	BufferWriteLocked = false;

	if (FlushBuffer)
		Flush ();

	return size;
}

//==============================================================================
// Reads a line from the stream. End of a line can be overridden..
//
//		char *Delimiters		End of line characters
//				"\r\n"			This is the default
//
//		bool FlushBuffer		Is the stream buffer flushed after reading?
//				true			The part of the buffer that is already read, is freed
//				false			The data already read is still stored
//
//	NOTE:: FlushBuffer has no effect when STREAM_ACCUM_BUF flag is set
//
//	Returns, what it was able to read from the stream until \r\n or EOF
//==============================================================================
string G3::FILESTREAM::Readline (string Delimiters, bool FlushBuffer) {
	string line;
	byte c = 0;
	int i = 0;

	Assert (Data, "FILESTREAM::Readline: Data member is a NULL pointer");

	// Is someone flushing / buffering already?
	if (BufferReadLocked)
		return 0;

	// Make sure nobody is moving our caret during the read
	CaretLocked = true;
	// Make sure the buffer data pointer stays constant during our read
	BufferWriteLocked = true;

	while (true) {
		if (CaretPosition == TotalSize)
			break;

		c = ((byte *) Data + CaretPosition - BufferPosition) [0];
		CaretPosition++;

		for (i=0; i < Delimiters.size (); i++) {
			if (c == Delimiters [i]) {
				LastLineDelimiter = c;
				goto filestream_readline1;
			}
		}

		line += c;
	}

// And here we go again with this spaghetti coding style..
filestream_readline1:

	// Free the caret to other threads
	CaretLocked = false;
	// Free the buffer to other threads
	BufferWriteLocked = false;

	if (FlushBuffer)
		Flush ();

	return line;
}

//==============================================================================
// Reads data from the stream and saves it into a file
//
//		const char *Filename	Name of the file to store the data in
//
//		bool FlushBuffer		Is the stream buffer flushed after reading?
//				true			The part of the buffer that is already read, is freed
//				false			The data already read is still stored
//
//		long Size				Number of bytes read from the stream
//				-1, 0			Reads the whole stream buffer
//
//		long Position			Where to write into the file
//
//	NOTE:: FlushBuffer has no effect when STREAM_ACCUM_BUF flag is set
//
//	Returns the number of bytes read
//==============================================================================
long G3::FILESTREAM::ReadToFile (const char *Filename, bool FlushBuffer, long Size, long Position) {
    long fsize = 0;
	long ChunkSize = Size;
	long BlankSize = 0;
	long ret = 0, i = 0;
	byte Blank = 0;
	byte *Data2 = NULL;

	Assert (Filename, "FILESTREAM::ReadToFile: Filename argument is a NULL pointer");

    fsize = GetFileSize (Filename);

	if (Size <= 0)
		ChunkSize = BufferedSize;

	if (Position > fsize)
		BlankSize = Position - fsize;

	Assert (Position >= 0, "FILESTREAM::ReadToFile: Position argument is invalid");

	FILE *fo = fopen (Filename, "wb");
	if (!fo) {
		if (Log)
			Log->Report ("ERROR! FILESTREAM::ReadToFile: Failed to open the file \"%s\"..\n", Filename);
		return 0;
	}

	fseek (fo, Position, SEEK_SET);

	for (i=0; i<BlankSize; i++)
		fwrite (&Blank, BlankSize, 1, fo);

	Data2 = new byte [ChunkSize + 1];

	ret = Read (Data2, ChunkSize, FlushBuffer);

	fwrite (Data2, ret, 1, fo);

	fclose (fo);

	return ret;
}

//==============================================================================
// Adds to the stream buffer
//
//		void *Pointer			Pointer to a memory block from where to buffer
//
//		long Size				Number of bytes to buffer
//
//	Returns true on success
//==============================================================================
long G3::FILESTREAM::Buffer (void *Pointer, long Size) {
	Assert (Pointer, "FILESTREAM::Buffer: Pointer argument is a NULL pointer");
	Assert (Size > 0, "FILESTREAM::Buffer: Size argument is invalid (<= 0)");

	if (BufferWriteLocked) {
		if (Log)
			Log->Report ("WARNING! FILESTREAM::Buffer: The buffer is locked.. Try again later..\n");
		return 0;
	}

	// Make sure nobody is flushing the buffer when we are buffering
	BufferWriteLocked = true;
	// Make sure nobody is reading the buffer (we may have to change the buffer pointer)
	BufferReadLocked = true;

	BufferedSize += Size;

	// We don't know the total length of this file?
	if (TotalSize <= BufferPosition + BufferedSize)
		TotalSize = BufferPosition + BufferedSize;

	// Reallocate memory for the buffer
	Data = realloc (Data, BufferedSize + 1);

	if (Data == NULL) {
		if (Log)
			Log->Report ("ERROR! FILESTREAM::Buffer: Not enough memory for buffering (at least %ld more bytes needed)\n", Size);

		// Free the buffer for flushing
		BufferWriteLocked = false;
		BufferReadLocked = false;
		return 0;
	}

	// "Buffer" the stream
	memcpy ((byte *) Data + BufferedSize - Size, Pointer, Size);

	((byte *) Data) [BufferedSize] = 0;

	// Free the buffer for flushing and for reading aswell
	BufferWriteLocked = false;
	BufferReadLocked = false;
	return Size;
}

//==============================================================================
// Sets the start of the file
//==============================================================================
void G3::FILESTREAM::SetFileStart () {
	FileStart = Tell (STREAM_SIZE);
}

//==============================================================================
// Buffers a file to the stream
//
//		const char *Filename	Name of the file to buffer
//
//		long Size				Number of bytes to buffer
//				-1, 0			Buffers the whole file into the stream
//
//		long Position			Where in the file should we start reading
//
//	Returns true on success
//==============================================================================
long G3::FILESTREAM::BufferFromFile (const char *Filename, long Size, long Position) {
	long fsize = 0;
	long ChunkSize = Size;
	long ret = 0;
	byte *Data2 = NULL;

	Assert (Filename, "FILESTREAM::BufferFromFile: Filename argument is a NULL pointer");

	fsize = GetFileSize (Filename);

    if (fsize < 1) {
        MainLog.Report ("FILESTREAM::BufferFromFile: The file \"%s\" does not exist\n", Filename);
        return 0;
    }

	Assert (Position >= 0 && Position < fsize, "FILESTREAM::BufferFromFile: Position argument is invalid (< 0 || >= Filesize)");

	// Make sure the ChunkSize is valid
	if (Size <= 0)
		ChunkSize = fsize - Position;
	if (Size > fsize)
		ChunkSize = fsize;

	// Try to open the file for binary reading
	FILE *fi = fopen (Filename, "rb");
	if (!fi) {
		if (Log)
			Log->Report ("ERROR! FILESTREAM::BufferFromFile: Failed to open the file \"%s\"..\n", Filename);
		return 0;
	}

	// Set the filename member
	LastFilename = Filename;
	// Set the start of the file
	SetFileStart ();

	// Make a temporary buffer
	Data2 = new byte [ChunkSize + 1];

	// Seek to the asked Position
	fseek (fi, Position, SEEK_SET);

	// Read the file
	ret = fread (Data2, 1, ChunkSize, fi);

	// Buffer the stream from our temporary buffer
	Buffer (Data2, ret);

	// Free our temporary buffer
	delete [] Data2;

	fclose (fi);

	// Set the size of the buffered file
	SetFileSize (ret);

	return ret;
}

//==============================================================================
// Enables a flag
//
//		byte Flag
//				STREAM_ACCUM_BUF	The whole file is stored in the memory
//==============================================================================
void G3::FILESTREAM::Enable (byte Flag) {
	Flags |= Flag;
}

//==============================================================================
// Disables a flag
//==============================================================================
void G3::FILESTREAM::Disable (byte Flag) {
	Flags &= (~Flag);
}

//==============================================================================
// Sets the limit for unflushed data. Regulates the minimum operative memory
// required for streaming. When more then Limit bytes data is already read
// and not needed anymore, it flushes.
//
//		long Limit					The limit should be big enough so that
//									the slow Flush () function is not called
//									too often. On the other hand, when the limit
//									is too heigh, the user has to manually Flush
//									the stream when it uses up too much memory..
//			STREAM_DEF_UNFLUSHED	This is the default limit 65536 (64 KB)
//==============================================================================
void G3::FILESTREAM::SetUnflushedLimit (long Limit) {
	MaximalUnflushed = Limit;
}

const void *G3::FILESTREAM::GetDataPointer () {
	return (const void *) Data;
}

long G3::GetFileSize (const char *Filename) {
    long ret = 0;

	Assert (Filename, "GetFileSize: Filename argument is a NULL pointer");

    FILE *f = fopen (Filename, "rb");
    if (f) {
        fseek (f, 0L, SEEK_END);
        ret = ftell (f);
        fclose (f);
    }
    return ret;
}
